/**
 * 
 */
package com.googlecode.cswish.struts.spring;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.Table;
import javax.persistence.Transient;

import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.hibernate.cfg.Configuration;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.SimpleValue;
import org.hibernate.util.StringHelper;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.annotation.DisplayOrder;
import com.googlecode.cswish.annotation.GenericType;
import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.model.PropertyInfo;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.hibernate.DynamicModel;
import com.googlecode.cswish.struts.hibernate.HibernateConfiguration;
import com.googlecode.cswish.util.ClassEx;
import com.opensymphony.xwork2.util.ClassLoaderUtil;

/**
 * Manager the bean meta data information
 * 
 * @author Jerry.Feng Date: 2008-4-9
 */
public class BeanIntrospector {
	private static final Logger logger = Logger.getLogger(BeanIntrospector.class);
	
	@Resource
	protected HibernateConfiguration hibernateConfiguration;
	
	@Resource
	protected ObjectFactory objectFactory;
	
	@Resource
	protected Config config;
	
	@Resource
	protected Routing routing;
	
	protected static final Map<String, ModelInfo> beanInfos = new HashMap<String, ModelInfo>();
	
	protected static final Map<String, List<Class>> classHierarchy = new HashMap<String, List<Class>>();

	@SuppressWarnings("unchecked")
	@PostConstruct
	private void initClassHierarchy() {
		for (Configuration cfg : hibernateConfiguration.getConfigurations()) {
			Iterator<PersistentClass> iter = cfg.getClassMappings();
			while (iter.hasNext()) {
				PersistentClass persistentClass = iter.next();
				String className = persistentClass.getClassName();
				try {
					Class clazz = ClassUtils.getClass(className);
					addClassHierarchy(getSupperClass(clazz), clazz);
				} catch (ClassNotFoundException ex) {
					logger.debug("type is not valid class:" + className);
				}
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	private List<Class> getSupperClass(Class clazz) {
		Class parent = clazz.getSuperclass();
		List<Class> parentClasses = new ArrayList<Class>();
		while (parent != null && parent != Object.class) {
			parentClasses.add(parent);
			parent = parent.getSuperclass();
		}
		return parentClasses;
	}
	
	@SuppressWarnings("unchecked")
	private void addClassHierarchy(List<Class> parentClasses, Class subClass) {
		for (Class parentClass : parentClasses) {
			List<Class> subClasses = classHierarchy.get(parentClass.getName());
			if (subClasses == null) {
				subClasses = new ArrayList<Class>();
				classHierarchy.put(parentClass.getName(), subClasses);
			}
			if (!subClasses.contains(subClass)) {
				subClasses.add(subClass);
			}
		}
	}
	
	public ModelInfo reset(String modelName) {
		String simpleName = getSimpleName(modelName);
		ModelInfo old = beanInfos.remove(modelName);
		beanInfos.remove(getModelName(simpleName));
		beanInfos.remove(getActionName(simpleName));
		return old;
	}
	
	private PersistentClass getPersistentClass(String className) {
		if (hibernateConfiguration != null) {
			return hibernateConfiguration.getClassMapping(className);
		} else {
			return null;
		}
	}
	
	public List<Class> getSubClass(String className) {
		return classHierarchy.get(className);
	}
	
	public boolean isHibernateModel(String className) {
		return getPersistentClass(className) != null;
	}
	
	public ModelInfo getModelInfo(String className) {
		return getModelInfo(className, true);
	}
	
	public String getPropertyType(String className, String nestedProperty) {
		String[] properties = StringUtils.split(nestedProperty, ".");
		String propertyType = className;
		for (String property : properties) {
			ModelInfo modelInfo = getModelInfo(propertyType);
			propertyType = modelInfo.getProperty(property).getGenericType();
		}
		return propertyType;
	}
	
	public boolean hasModel(String className) {
		return getModelInfo(className, false) != null;
	}
	
	public String getDescriptor(String className, String nestedPropertyName) {
		if (nestedPropertyName == null) {
			return className;
		}
		
		String[] propertyNames = StringUtils.split(nestedPropertyName, '.');
		
		String modelName = className;
		Class<?> descriptor = null;
		for (int i = 0; modelName != null && i < propertyNames.length; i++) {
			ModelInfo modelInfo = getModelInfo(modelName);
			if (modelInfo != null) {
				String propertyName = propertyNames[i];
				if (propertyName.charAt(propertyName.length() - 1) == ']')	{// [x]
					propertyName = propertyName.substring(0, propertyName.lastIndexOf('['));
					descriptor = getDescriptor(modelInfo, propertyName);
				} else {
					descriptor = getDescriptor(modelInfo, propertyName);
				}
				if (descriptor != null) {
					modelName = descriptor.getName();
				} else {
					modelName = null;
				}
			}
		}
		return descriptor == null ? null : descriptor.getName();
	}
	
	private Class<?> getDescriptor(ModelInfo modelInfo, String propertyName) {
		PropertyInfo propertyInfo = modelInfo.getProperty(propertyName);
		if (propertyInfo != null) {
			return propertyInfo.getGenericTypeClass();
		}
		return null;
	}
	
	/**
	 * name = config.appPackage + simpleName or simpleName 
	 * 
	 * @param name
	 * @return
	 */
	public ModelInfo getModelInfo(String className, boolean showDebug) {
		ModelInfo modelInfo = beanInfos.get(className);
		if (modelInfo != null) {
			return modelInfo;
		}
		
		modelInfo = createModelInfo(className, showDebug);
		
		if (modelInfo != null) {
			
			// cache it, avoid dead-cycle when updating reference
			String simpleName = getSimpleName(className);
			
			beanInfos.put(className, modelInfo);
			beanInfos.put(simpleName, modelInfo);
		}
		return modelInfo;
	}
	
	protected ModelInfo createModelInfo(String className, boolean showDebug) {
		// create model information
		String simpleName = getSimpleName(className);

		Class<?> modelClass;
		try {
			// objectFactory.getClassInstance(className);
			modelClass = ClassLoaderUtil.loadClass(className, this.getClass());
		} catch (Throwable t) {
			modelClass = null;
		}
		
		PersistentClass persistentClass = getPersistentClass(className);
		ModelInfo modelInfo;
		
		// TODO: Refactor the following codes later
		if (modelClass != null) {
			Map<String, Integer> orders = new HashMap<String, Integer>();
			
			int orderIndex = 0;
			// the init order comes from the constructor
			Constructor<?>[] constructors = modelClass.getConstructors();
			if (constructors.length > 0) {
				Constructor selectedConstruct = constructors[0];
				int curConstructLen = selectedConstruct.getParameterTypes().length;
				for (int i = 1; i < constructors.length; i++) {
					if (constructors[i].getParameterTypes().length > curConstructLen) {
						selectedConstruct = constructors[i];
					}
				}
				String[] paramNames = ClassEx.getParameterNames(selectedConstruct);
				for (int i = 0; i < paramNames.length; i++) {
					String name = paramNames[i];
					orders.put(name, i);
				}
				orderIndex = paramNames.length;
			}
			
			Method[] methods = modelClass.getMethods();
			for (int i = 0; i < methods.length; i++) {
				Method method = methods[i];
				DisplayOrder order = method.getAnnotation(DisplayOrder.class);
				if (order != null) {
					orders.put(method.getName(), order.value());
					// TODO: load the field annotation definition?
				}
			}
			
			modelInfo = new ModelInfo();
			modelInfo.setType(className);
			modelInfo.setSimpleName(simpleName);
			updateTableName(modelInfo, modelClass, persistentClass);
			
			try {
				BeanInfo beanInfo = Introspector.getBeanInfo(modelClass);
				Map<String, String> actualTypeArguments = ClassEx.getActualTypeArguments(modelClass);
				
		        PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
				for (PropertyDescriptor descriptor : descriptors) {
					String propertyName = descriptor.getDisplayName();
					Class<?> type = descriptor.getPropertyType();
					if ("class".equals(propertyName) || type == null) {	// skip the property
						continue;
					}
		
					PropertyInfo propertyInfo = modelInfo.getProperty(propertyName);
					boolean isNewProperty = false;
					if (propertyInfo == null) {
						propertyInfo = new PropertyInfo();
						propertyInfo.setName(propertyName);
						isNewProperty = true;
					}
					
					propertyInfo.setType(type.getName());
					Method readMethod = descriptor.getReadMethod();
					Method writeMethod = descriptor.getWriteMethod();
					Integer order = null;
					if (readMethod != null) {
						GenericType sign = readMethod.getAnnotation(GenericType.class);
						if (sign != null) {
							// the priority of hbm is higher 
							propertyInfo.setGenericType(sign.value());
						} else {
							try {
								Type retType = readMethod.getGenericReturnType();
								String genericType = ClassEx.getGenericType(retType, actualTypeArguments);
								if (genericType != null) {
									propertyInfo.setGenericType(genericType);
								}
							} catch(Throwable t) {
							}
						}
						Integer theOrder = orders.get(propertyName);
						if (theOrder == null) {
							order = orders.get(readMethod.getName());
						} else {
							order = theOrder;
						}
					} else if (writeMethod != null) {
						Integer theOrder = orders.get(propertyName);
						if (theOrder == null) {
							order = orders.get(writeMethod.getName());
						} else {
							order = theOrder;
						}
					}
					propertyInfo.setTabIndex(order == null ? Integer.MAX_VALUE : order.intValue());
					
					propertyInfo.setReadMethod(readMethod);
					propertyInfo.setWriteMethod(writeMethod);
					boolean isSimpleObj = isSimpleObj(type);
					propertyInfo.setSimpleObj(isSimpleObj);
					
					// searchInRange
					PropertyInfo sibling;
					if (propertyName.charAt(propertyName.length() - 1) == '2') {
						sibling = modelInfo.getProperty(propertyName.substring(0, propertyName.length() - 1));
					} else {
						sibling = modelInfo.getProperty(propertyName + '2');
					}
					if (sibling != null) {
						propertyInfo.setSearchInRange(true);
						sibling.setSearchInRange(true);
					}
					
					updateTableProperty(propertyInfo, null, persistentClass);
					
					if (isNewProperty) {
						modelInfo.addProperty(null, propertyInfo, true, propertyInfo.isTableColumn());
					}
				}//for each descriptors
			} catch (IntrospectionException ex) {
	        	logger.error("Can't get bean information:" + modelClass.getName(), ex);
	        }
		} else if (persistentClass != null) {
			modelInfo = new ModelInfo();
			modelInfo.setType(DynamicModel.class.getName());
			modelInfo.setGenericType(className);
			modelInfo.setSimpleName(simpleName);
			
			updateTableName(modelInfo, modelClass, persistentClass);
			Iterator<Property> iter = persistentClass.getPropertyClosureIterator();
			Property identifierProperty = persistentClass.getIdentifierProperty();
			int index = 0;
			do {
				Property property;
				if (identifierProperty != null) {	// first element is the id property
					property = identifierProperty;
					identifierProperty = null;
				} else {
					property = iter.next();
				}
				index++;
				
				String propertyName = property.getName();
				PropertyInfo propertyInfo = modelInfo.getProperty(propertyName);
				boolean isNewProperty = false;
				if (propertyInfo == null) {
					propertyInfo = new PropertyInfo();
					propertyInfo.setName(propertyName);
					isNewProperty = true;
				}
				propertyInfo.setDynamic(true);
				Class<?> type = property.getType().getReturnedClass();
				propertyInfo.setType(type.getName());
				propertyInfo.setTabIndex(index);
				
				boolean isSimpleObj = type.isPrimitive()
						|| Number.class.isAssignableFrom(type)
						|| String.class.equals(type)
						|| Date.class.isAssignableFrom(type);
				propertyInfo.setSimpleObj(isSimpleObj);

				// searchInRange
				PropertyInfo sibling;
				if (propertyName.charAt(propertyName.length() - 1) == '2') {
					sibling = modelInfo.getProperty(propertyName.substring(0,
							propertyName.length() - 1));
				} else {
					sibling = modelInfo.getProperty(propertyName + '2');
				}
				if (sibling != null) {
					propertyInfo.setSearchInRange(true);
					sibling.setSearchInRange(true);
				}

				updateTableProperty(propertyInfo, property, persistentClass);
				
				if (isNewProperty) {
					modelInfo.addProperty(null, propertyInfo, true, propertyInfo.isTableColumn());
				}
			} while (iter.hasNext());
		} else {
			if (showDebug && logger.isDebugEnabled()) {
				logger.debug("Can't find static model:" + className);
			}
			modelInfo = null;
		}
		return modelInfo;
	}
	
	protected boolean isSimpleObj(Class<?> type) {
		boolean isSimpleObj = type.isPrimitive() || Number.class.isAssignableFrom(type)
			|| Date.class.isAssignableFrom(type) || type.getName().startsWith("java.lang.");
		return isSimpleObj;
	}

	protected void updateTableProperty(PropertyInfo propertyInfo, Property property, PersistentClass persistentClass) {
		int index = 0;
		if (property == null && persistentClass != null) {
			// find the property and the position index
			String propertyName = propertyInfo.getName();
			Property identifierProperty = persistentClass.getIdentifierProperty();
			if (identifierProperty != null
					&& identifierProperty.getName().equals(StringHelper.root(propertyName))
					) {
				property = identifierProperty;
			} else {
				Iterator iterator = persistentClass.getPropertyClosureIterator();
				while (iterator.hasNext() ) {
					index++;
					Property prop = (Property) iterator.next();
					if (prop.getName().equals(StringHelper.root(propertyName) ) ) {
						property = prop;
						break;
					}
				}
			}
		}
		
		if (property != null) {
			if (index != 0) {
				propertyInfo.setTabIndex(index);
			}
			
//			Value value = property.getValue();
//			if (value != null) {
//				Value element = null;
//				if (value instanceof Bag) {
//					Bag bag = (Bag) value;
//					element = bag.getElement();
//				} else if (value instanceof Set) {
//					Set set = (Set) value;
//					element = set.getElement();
//				}
//				if (element != null && element instanceof OneToMany) {
//					OneToMany oneToMany = (OneToMany) element;
//					
//					// TODO: call ClassEx to get generic parameter type
//					propertyInfo.setGenericType(oneToMany.getReferencedEntityName());
//				}
//			}
			
			propertyInfo.setTableColumn(true);
			Iterator columnIter = property.getColumnIterator();
			if (columnIter.hasNext()) {
				org.hibernate.mapping.Column column = (org.hibernate.mapping.Column)columnIter.next();
				
				boolean isId = persistentClass.getIdentifierProperty() == property;
				propertyInfo.setId(isId);
				if (isId) {
					SimpleValue idValue = (SimpleValue) property.getValue();
					String generatorStrategy = idValue.getIdentifierGeneratorStrategy();
					boolean autoId = !"assigned".equals(generatorStrategy);
					propertyInfo.setAutoId(autoId);
				}
				propertyInfo.setColumnName(column.getName()); 
				// 255 is not the real length of DB column
				if (column.getLength() != org.hibernate.mapping.Column.DEFAULT_LENGTH) {
					propertyInfo.setLength(column.getLength());
				}
				propertyInfo.setUnique(column.isUnique());
				propertyInfo.setNullable(column.isNullable());
			} else {
				propertyInfo.setColumnName(property.getName());
			}
			propertyInfo.setInsertable(property.isInsertable());
			propertyInfo.setUpdatable(property.isUpdateable());
		} else {
			// TODO: check the field annotations?
			Method method = propertyInfo.getReadMethod();
			if (method != null) {
				Column column = method.getAnnotation(Column.class);
				boolean isTransient = persistentClass == null 
				&& method.getDeclaringClass().getAnnotation(Embeddable.class) == null
					|| method.getAnnotation(Transient.class) != null;
				propertyInfo.setTableColumn(!isTransient);
				
				if (column != null) {
					propertyInfo.setColumnName(column.name());
					// 255 is not the real length of DB column
					if (column.length() != org.hibernate.mapping.Column.DEFAULT_LENGTH) {
						propertyInfo.setLength(column.length());
					}
					propertyInfo.setUnique(column.unique());
					propertyInfo.setNullable(column.nullable());
					propertyInfo.setInsertable(column.insertable());
					propertyInfo.setUpdatable(column.updatable());
				}
			}
		}
	}
	
	protected void updateTableName(ModelInfo modelInfo, Class<?> modelClass, PersistentClass persistentClass) {
		String tableName = null;
		if (persistentClass != null) {
			tableName = persistentClass.getTable().getName();
		} else {
			if (modelClass != null) {
				Table table = modelClass.getAnnotation(Table.class);
				if (table != null) {
					tableName = table.name();
				}
			}
		}
		
		modelInfo.setTable(tableName);
	}
	
	
	
	/**
	 * Check a class if it's a model
	 * 
	 * @param className
	 * @return
	 */
	public int isModel(String className) {
		return getMatchedIndex(className, config.getModelPackages(), 
				config.getModelPackageSuffixs());
	}
	
	/**
	 * Check a class if it's a action
	 * 
	 * @param className
	 * @return
	 */
	public int isAction(String className) {
		return getMatchedIndex(className, config.getActionPackages(), 
				config.getActionPackageSuffixs());
	}
	/**
	 * Check a class if it's a service
	 * 
	 * @param className
	 * @return
	 */
	public int isService(String className) {
		return getMatchedIndex(className, config.getServicePackages(), 
				config.getServicePackageSuffixs());
	}

	private int getMatchedIndex(String className, String[] packages,
			String[] packageSuffixs) {
		for (int i = 0; i < packageSuffixs.length; i++) {
			String s = packages[i];
			if (className.startsWith(s) && className.endsWith(packageSuffixs[i])) {
				return i;
			}
		}
		return -1;
	}
	
	/**
	 * Get the simple name according to naming rules:
	 * <li> Action = Action package + simple name + method + suffix
	 * <li> Service = Application package + simple name + suffix
	 * <li> Model = Model package + simple name + suffix
	 * 
	 * @param actionName
	 * @return
	 */
	public String getSimpleName(String className) {
		int index;
		String simpleName;
		if ((index = isAction(className)) >= 0) {
			String theName = className.substring(config.getActionPackages()[index].length(),
					className.length() - config.getActionPackageSuffixs()[index].length());
			int separator = routing.findMethodSeparator(theName);
			
			if (separator > 0) {
				simpleName = theName.substring(0, separator);
			} else {
				simpleName = theName;
			}
		} else if ((index = isService(className)) >= 0) {
			simpleName = className.substring(config.getServicePackages()[index].length(),
					className.length() - config.getServicePackageSuffixs()[index].length());
		} else if ((index = isModel(className)) >= 0) {
			simpleName = className.substring(config.getModelPackages()[index].length(),
					className.length() - config.getModelPackageSuffixs()[index].length());
		} else {
			simpleName = className;
		}
		
		int separator = simpleName.lastIndexOf('.') + 1;
		if (Character.isUpperCase(simpleName.charAt(separator))) {
			simpleName = simpleName.substring(0, separator)
				+ Character.toLowerCase(simpleName.charAt(separator))
				+ simpleName.substring(separator + 1);
		}
		return simpleName;
	}
	
	/**
	 * Get model name according to the simple name
	 * 
	 * @param simpleName
	 * @return
	 */
	public String getModelName(String simpleName) {
		if (isModel(simpleName) >= 0) {
			return simpleName;
		} else {
			String modelPackage = config.getModelPackage();
			return buildClassName(modelPackage, simpleName, 
					config.getModelPackageSuffixs()[0]);
		}
	}

	private String buildClassName(String packageName, String simpleName, String suffix) {
		int index = simpleName.lastIndexOf('.') + 1;
		if (Character.isLowerCase(simpleName.charAt(index))) {
			return packageName + simpleName.substring(0, index)
				+ Character.toUpperCase(simpleName.charAt(index))
				+ simpleName.substring(index + 1) + suffix;
		} else {
			return packageName + simpleName + suffix;
		}
	}
	
	/**
	 * Get action name according to the simple name
	 * 
	 * @param simpleName
	 * @return
	 */
	public String getActionName(String simpleName) {
		if (isAction(simpleName) > 0) {
			return simpleName;
		} else {
			String actionPackage = config.getActionPackage();
			return buildClassName(actionPackage, simpleName,
					config.getActionPackageSuffixs()[0]);
		}
	}
	
	/**
	 * Get action name according to the simple name
	 * 
	 * @param simpleName
	 * @return
	 */
	public String getServiceName(String serviceName) {
		if (isService(serviceName) > 0) {
			return serviceName;
		} else {
			String servicePackage = config.getServicePackage();
			return buildClassName(servicePackage, serviceName,
					config.getServicePackageSuffixs()[0]);
		}
	}
}
